class Emitter {
  constructor() {
    this._emitter = {}
  }

  on(event, callback, thisArg = null) {
    const callbacks = this._emitter[event]

    Array.isArray(callbacks)
      ? callbacks.push({ fn: callback, thisArg })
      : (this._emitter[event] = [{ fn: callback, thisArg }])

    return this
  }

  emit(event, ...args) {
    if (Array.isArray(this._emitter[event])) {
      const callbacks = this._emitter[event]
      const onceIndexSet = []

      callbacks.forEach(({ fn, thisArg, once }, index, arr) => {
        fn.apply(thisArg, args)
        once && onceIndexSet.push(index)
      })

      if (callbacks.length === onceIndexSet.length) {
        delete this._emitter[event]
      } else {
        for (let i = onceIndexSet.length - 1; i > -1; i--) callbacks.splice(onceIndexSet[i], 1)
      }
    }

    return this
  }

  off(event, callback) {
    if (!event) {
      this._emitter = {}
      return this
    }

    if (!callback) {
      delete this._emitter[event]
      return this
    }

    const callbacks = this._emitter[event]
    if (Array.isArray(callbacks)) {
      // There may be the same callback for an event, but just remove one
      const index = callbacks.findIndex(({ fn }) => fn === callback)
      ~index && (callbacks.length === 1 ? delete this._emitter[event] : callbacks.splice(index, 1))
    }

    return this
  }

  once(event, callback, thisArg = null) {
    const callbacks = this._emitter[event]

    Array.isArray(callbacks)
      ? callbacks.push({ fn: callback, thisArg, once: true })
      : (this._emitter[event] = [{ fn: callback, thisArg, once: true }])

    return this
  }
}

export function createEmitter() {
  return new Emitter()
}
